#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 ZHENG Zhong <heavyzheng nospam-at gmail D0T com>
# http://www.zhengzhong.net/
#
# This script is used to generate a stats report for projects.
# 
# $Id: srcstats.py 318 2009-03-02 14:03:21Z heavyzheng $
#

from __future__ import with_statement

import logging
import os
import re
import sys
import traceback

from optparse import OptionParser


#---------------------------------------------------------------------------------------------------
# StatsError
#---------------------------------------------------------------------------------------------------

class StatsError(Exception):
    pass


#---------------------------------------------------------------------------------------------------
# Common functions
#---------------------------------------------------------------------------------------------------

def _count_lines(path):
    lines = 0
    try:
        with open(path, "r") as f:
            lines = len(f.readlines())
    except:
        pass
    return lines

def _to_size(bytes):
    if bytes >= 1024 * 1024:
        size_str = "%.2f M" % (float(bytes) / (1024 * 1024))
    elif bytes >= 1024:
        size_str = "%.2f K" % (float(bytes) / 1024)
    else:
        size_str = "%d B" % bytes
    return size_str


#---------------------------------------------------------------------------------------------------
# ProjectStats class
#---------------------------------------------------------------------------------------------------

_SRC_PATTERNS = {
    "cpp"    : r".*\.(c|cpp|cc|h|hpp|hh)$",
    "java"   : r".*\.java$",
    "csharp" : r".*\.cs$",
    "python" : r".*\.py$",
} # _SRC_PATTERNS

_RES_PATTERN = r".*\.(dox|txt|xml|yaml)$"


class ProjectStats(object):

    def __init__(self, name, lang):
        self.name = name
        self.lang = lang
        self.ignored = []   # Total number of ignored files.
        self.src_files = [] # Total number of source files.
        self.src_bytes = 0  # Total number of bytes in source files.
        self.src_lines = 0  # Total number of lines in source files.
        self.res_files = [] # Total number of text files.
        self.res_bytes = 0  # Total number of bytes in text files.
        self.res_lines = 0  # Total number of lines in text files.
        
        src_pattern = _SRC_PATTERNS.get(lang)
        if not src_pattern:
            raise StatsError, "Invalid project language: %s" % lang
        self.src_regex = re.compile(src_pattern)
        self.res_regex = re.compile(_RES_PATTERN)
    
    def _is_source(self, path):
        if self.src_regex.search(path.lower()):
            return True
        else:
            return False
    
    def _is_resource(self, path):
        if self.res_regex.search(path.lower()):
            return True
        else:
            return False
    
    def analyse(self, path):
        if self._is_source(path):
            logging.debug("[SRC] %s" % path)
            self.src_files.append(path)
            self.src_bytes += os.path.getsize(path)
            self.src_lines += _count_lines(path)
        elif self._is_resource(path):
            logging.debug("[RES] %s" % path)
            self.res_files.append(path)
            self.res_bytes += os.path.getsize(path)
            self.res_lines += _count_lines(path)
        else:
            logging.debug("[---] %s" % path)
            self.ignored.append(path)
    
    def _print_stats(self, stream, details):
    
        # Print stats report header.
        print >> stream, "-" * 80
        print >> stream, "Project stats for: %s (lang: %s)" % (self.name, self.lang)
        print >> stream, ""
        
        # Print stats of source files.
        file_count = len(self.src_files)
        print >> stream, "%d source files analysed ..." % file_count
        if file_count > 0:
            print >> stream, "- Total lines     : %d" % self.src_lines
            print >> stream, "- Ave. lines/file : %d" % (self.src_lines / file_count)
            print >> stream, "- Total size      : %s" % _to_size(self.src_bytes)
            print >> stream, "- Ave. size/file  : %s" % _to_size(self.src_bytes / file_count)
            if details:
                print >> stream, "- Source files analysed : %s" % self.src_regex.pattern
                for path in self.src_files:
                    print >> stream, "  - %s" % path
        print >> stream, ""
        
        # Print stats of resource files.
        file_count = len(self.res_files)
        print >> stream, "%d resource files analysed ..." % file_count
        if file_count > 0:
            print >> stream, "- Total lines     : %d" % self.res_lines
            print >> stream, "- Ave. lines/file : %d" % (self.res_lines / file_count)
            print >> stream, "- Total size      : %s" % _to_size(self.res_bytes)
            print >> stream, "- Ave. size/file  : %s" % _to_size(self.res_bytes / file_count)
            if details:
                print >> stream, "- Resource files analysed : %s" % self.res_regex.pattern
                for path in self.res_files:
                    print >> stream, "  - %s" % path
        print >> stream, ""
        
        # Print stats of ignored files.
        file_count = len(self.ignored)
        print >> stream, "%d files ignored." % file_count
        if file_count > 0 and details:
            print >> stream, "- Files ignored:"
            for path in self.ignored:
                print >> stream, "  - %s" % path
        print >> stream, ""
        
        # Print stats report footer.
        print >> stream, "-" * 80
    
    def print_summary(self, stream=sys.stdout):
        self._print_stats(stream=stream, details=False)
    
    def print_details(self, stream=sys.stdout):
        self._print_stats(stream=stream, details=True)


#---------------------------------------------------------------------------------------------------
# Main functions
#---------------------------------------------------------------------------------------------------

def _create_option_parser():
    """Creates an option parser to parse command-line arguments."""

    parser = OptionParser(usage="""%prog [OPTIONS] SOURCE_DIRECTORY
    Generate stats report for the specified SOURCE_DIRECTORY.""")
    parser.add_option( "-l", "--lang",
                       dest="lang",
                       metavar="LANG",
                       help="specify the project language (cpp, java, csharp, python, etc.)" )
    parser.add_option( "-d", "--details",
                       dest="details",
                       default=False,
                       action="store_true",
                       help="show stats report details" )
    parser.add_option( "-q", "--quiet",
                       dest="quiet",
                       default=False,
                       action="store_true",
                       help="set to quiet mode, print only warnings and errors" )
    parser.add_option( "-v", "--verbose",
                       dest="verbose",
                       default=False,
                       action="store_true",
                       help="set to verbose mode, print detailed messages" )
    return parser


def main(argv):

    # Parse command-line arguments to opts and args.
    parser = _create_option_parser()
    (opts, args) = parser.parse_args(argv)

    if opts.lang and len(args) == 1:
    
        # Configure logging, set logging level.
        logging.basicConfig(format="%(levelname)-7s: %(message)s")
        if opts.quiet:
            logging.getLogger().setLevel(logging.WARNING)
        elif opts.verbose:
            logging.getLogger().setLevel(logging.DEBUG)
        else:
            logging.getLogger().setLevel(logging.INFO)
        
        try:
            # Check the input source code directory exists.
            src_dir = args[0]
            if not os.path.isdir(src_dir):
                raise Exception, "The source directory '%s' does not exist." % src_dir
            
            # Create a ProjectStats object to hold stats report.
            project_name = os.path.basename(src_dir)
            project_lang = opts.lang.lower()
            stats = ProjectStats(project_name, project_lang)
            logging.debug("Analysing project: %s (%s)" % (project_name, project_lang))
            
            # Walk through the source directory tree.
            for dir_path, dir_names, file_names in os.walk(src_dir):
                for file_name in file_names:
                    stats.analyse(os.path.join(dir_path, file_name))
            
            # End of analysing.
            logging.debug("Source stats generated for project: %s" % project_name)
            if opts.details:
                stats.print_details()
            else:
                stats.print_summary()
        
        except Exception, exc:
            logging.error("Abort: an error has occurred: %s" % str(exc))
            # Print exception traceback.
            print >> sys.stderr, "-" * 80
            traceback.print_exc()
            print >> sys.stderr, "-" * 80
    
    else:
    
        # Invalid command-line arguments: print error message(s) and help.
        print >> sys.stderr, "\n"
        if not opts.lang:
            print >> sys.stderr, "Error: project language not specified."
        if len(args) < 1:
            print >> sys.stderr, "Error: source directory not specified."
        elif len(args) > 1:
            print >> sys.stderr, "Error: too many source directories."
        print >> sys.stderr, "\n"
        parser.print_help()


#---------------------------------------------------------------------------------------------------

if __name__ == "__main__":
    main(sys.argv[1:])



